#pragma once
#include <Windows.h>
#include "async_message_queue.h"
#include <WinSafer.h>
#include <Sddl.h>
#include <accctrl.h>
#include <aclapi.h>

class TwoWayPipeMessageIPC
{
public:
    typedef void (*callback_function)(const std::wstring&);
    void send(std::wstring msg)
    {
        output_queue.queue_message(msg);
    }
    TwoWayPipeMessageIPC(std::wstring _input_pipe_name, std::wstring _output_pipe_name, callback_function p_func)
    {
        input_pipe_name = _input_pipe_name;
        output_pipe_name = _output_pipe_name;
        dispatch_inc_message_function = p_func;
    }
    void start(HANDLE _restricted_pipe_token)
    {
        output_queue_thread = std::thread(&TwoWayPipeMessageIPC::consume_output_queue_thread, this);
        input_queue_thread = std::thread(&TwoWayPipeMessageIPC::consume_input_queue_thread, this);
        input_pipe_thread = std::thread(&TwoWayPipeMessageIPC::start_named_pipe_server, this, _restricted_pipe_token);
    }

    void end()
    {
        closed = true;
        input_queue.interrupt();
        input_queue_thread.join();
        output_queue.interrupt();
        output_queue_thread.join();
        pipe_connect_handle_mutex.lock();
        if (current_connect_pipe_handle != NULL)
        {
            //Cancels the Pipe currently waiting for a connection.
            CancelIoEx(current_connect_pipe_handle, NULL);
        }
        pipe_connect_handle_mutex.unlock();
        input_pipe_thread.join();
    }

private:
    AsyncMessageQueue input_queue;
    AsyncMessageQueue output_queue;
    std::wstring output_pipe_name;
    std::wstring input_pipe_name;
    std::thread input_queue_thread;
    std::thread output_queue_thread;
    std::thread input_pipe_thread;
    std::mutex pipe_connect_handle_mutex; // For manipulating the current_connect_pipe

    HANDLE current_connect_pipe_handle = NULL;
    bool closed = false;
    TwoWayPipeMessageIPC::callback_function dispatch_inc_message_function;
    const DWORD BUFSIZE = 1024;

    void send_pipe_message(std::wstring message)
    {
        // Adapted from https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipe-client
        HANDLE output_pipe_handle;
        const wchar_t* message_send = message.c_str();
        BOOL fSuccess = FALSE;
        DWORD cbToWrite, cbWritten, dwMode;
        const wchar_t* lpszPipename = output_pipe_name.c_str();

        // Try to open a named pipe; wait for it, if necessary.

        while (1)
        {
            output_pipe_handle = CreateFile(
                lpszPipename, // pipe name
                GENERIC_READ | // read and write access
                    GENERIC_WRITE,
                0, // no sharing
                NULL, // default security attributes
                OPEN_EXISTING, // opens existing pipe
                0, // default attributes
                NULL); // no template file

            // Break if the pipe handle is valid.

            if (output_pipe_handle != INVALID_HANDLE_VALUE)
                break;

            // Exit if an error other than ERROR_PIPE_BUSY occurs.
            DWORD curr_error = 0;
            if ((curr_error = GetLastError()) != ERROR_PIPE_BUSY)
            {
                return;
            }

            // All pipe instances are busy, so wait for 20 seconds.

            if (!WaitNamedPipe(lpszPipename, 20000))
            {
                return;
            }
        }
        dwMode = PIPE_READMODE_MESSAGE;
        fSuccess = SetNamedPipeHandleState(
            output_pipe_handle, // pipe handle
            &dwMode, // new pipe mode
            NULL, // don't set maximum bytes
            NULL); // don't set maximum time
        if (!fSuccess)
        {
            return;
        }

        // Send a message to the pipe server.

        cbToWrite = (lstrlen(message_send)) * sizeof(WCHAR); // no need to send final '\0'. Pipe is in message mode.

        fSuccess = WriteFile(
            output_pipe_handle, // pipe handle
            message_send, // message
            cbToWrite, // message length
            &cbWritten, // bytes written
            NULL); // not overlapped
        if (!fSuccess)
        {
            return;
        }
        CloseHandle(output_pipe_handle);
        return;
    }

    void consume_output_queue_thread()
    {
        while (!closed)
        {
            std::wstring message = output_queue.pop_message();
            if (message.length() == 0)
            {
                break;
            }
            send_pipe_message(message);
        }
    }

    BOOL GetLogonSID(HANDLE hToken, PSID* ppsid)
    {
        // From https://docs.microsoft.com/en-us/previous-versions/aa446670(v=vs.85)
        BOOL bSuccess = FALSE;
        DWORD dwIndex;
        DWORD dwLength = 0;
        PTOKEN_GROUPS ptg = NULL;

        // Verify the parameter passed in is not NULL.
        if (NULL == ppsid)
            goto Cleanup;

        // Get required buffer size and allocate the TOKEN_GROUPS buffer.

        if (!GetTokenInformation(
                hToken, // handle to the access token
                TokenGroups, // get information about the token's groups
                (LPVOID)ptg, // pointer to TOKEN_GROUPS buffer
                0, // size of buffer
                &dwLength // receives required buffer size
                ))
        {
            if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
                goto Cleanup;

            ptg = (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(),
                                           HEAP_ZERO_MEMORY,
                                           dwLength);

            if (ptg == NULL)
                goto Cleanup;
        }

        // Get the token group information from the access token.

        if (!GetTokenInformation(
                hToken, // handle to the access token
                TokenGroups, // get information about the token's groups
                (LPVOID)ptg, // pointer to TOKEN_GROUPS buffer
                dwLength, // size of buffer
                &dwLength // receives required buffer size
                ))
        {
            goto Cleanup;
        }

        // Loop through the groups to find the logon SID.

        for (dwIndex = 0; dwIndex < ptg->GroupCount; dwIndex++)
            if ((ptg->Groups[dwIndex].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID)
            {
                // Found the logon SID; make a copy of it.

                dwLength = GetLengthSid(ptg->Groups[dwIndex].Sid);
                *ppsid = (PSID)HeapAlloc(GetProcessHeap(),
                                         HEAP_ZERO_MEMORY,
                                         dwLength);
                if (*ppsid == NULL)
                    goto Cleanup;
                if (!CopySid(dwLength, *ppsid, ptg->Groups[dwIndex].Sid))
                {
                    HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);
                    goto Cleanup;
                }
                break;
            }

        bSuccess = TRUE;

    Cleanup:

        // Free the buffer for the token groups.

        if (ptg != NULL)
            HeapFree(GetProcessHeap(), 0, (LPVOID)ptg);

        return bSuccess;
    }

    VOID FreeLogonSID(PSID* ppsid)
    {
        // From https://docs.microsoft.com/en-us/previous-versions/aa446670(v=vs.85)
        HeapFree(GetProcessHeap(), 0, (LPVOID)*ppsid);
    }

    int change_pipe_security_allow_restricted_token(HANDLE handle, HANDLE token)
    {
        PACL old_dacl, new_dacl;
        PSECURITY_DESCRIPTOR sd;
        EXPLICIT_ACCESS ea;
        PSID user_restricted;
        int error;

        if (!GetLogonSID(token, &user_restricted))
        {
            error = 5; // No access error.
            goto Ldone;
        }

        if (GetSecurityInfo(handle,
                            SE_KERNEL_OBJECT,
                            DACL_SECURITY_INFORMATION,
                            NULL,
                            NULL,
                            &old_dacl,
                            NULL,
                            &sd))
        {
            error = GetLastError();
            goto Lclean_sid;
        }

        memset(&ea, 0, sizeof(EXPLICIT_ACCESS));
        ea.grfAccessPermissions |= GENERIC_READ | FILE_WRITE_ATTRIBUTES;
        ea.grfAccessPermissions |= GENERIC_WRITE | FILE_READ_ATTRIBUTES;
        ea.grfAccessPermissions |= SYNCHRONIZE;
        ea.grfAccessMode = SET_ACCESS;
        ea.grfInheritance = NO_INHERITANCE;
        ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
        ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
        ea.Trustee.ptstrName = (LPTSTR)user_restricted;

        if (SetEntriesInAcl(1, &ea, old_dacl, &new_dacl))
        {
            error = GetLastError();
            goto Lclean_sd;
        }

        if (SetSecurityInfo(handle,
                            SE_KERNEL_OBJECT,
                            DACL_SECURITY_INFORMATION,
                            NULL,
                            NULL,
                            new_dacl,
                            NULL))
        {
            error = GetLastError();
            goto Lclean_dacl;
        }

        error = 0;

    Lclean_dacl:
        LocalFree((HLOCAL)new_dacl);
    Lclean_sd:
        LocalFree((HLOCAL)sd);
    Lclean_sid:
        FreeLogonSID(&user_restricted);
    Ldone:
        return error;
    }

    HANDLE create_medium_integrity_token()
    {
        HANDLE restricted_token_handle;
        SAFER_LEVEL_HANDLE level_handle = NULL;
        DWORD sid_size = SECURITY_MAX_SID_SIZE;
        BYTE medium_sid[SECURITY_MAX_SID_SIZE];
        if (!SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, SAFER_LEVEL_OPEN, &level_handle, NULL))
        {
            return NULL;
        }
        if (!SaferComputeTokenFromLevel(level_handle, NULL, &restricted_token_handle, 0, NULL))
        {
            SaferCloseLevel(level_handle);
            return NULL;
        }
        SaferCloseLevel(level_handle);

        if (!CreateWellKnownSid(WinMediumLabelSid, nullptr, medium_sid, &sid_size))
        {
            CloseHandle(restricted_token_handle);
            return NULL;
        }

        TOKEN_MANDATORY_LABEL integrity_level = { 0 };
        integrity_level.Label.Attributes = SE_GROUP_INTEGRITY;
        integrity_level.Label.Sid = reinterpret_cast<PSID>(medium_sid);

        if (!SetTokenInformation(restricted_token_handle, TokenIntegrityLevel, &integrity_level, sizeof(integrity_level)))
        {
            CloseHandle(restricted_token_handle);
            return NULL;
        }

        return restricted_token_handle;
    }

    void handle_pipe_connection(HANDLE input_pipe_handle)
    {
        //Adapted from https://docs.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server
        HANDLE hHeap = GetProcessHeap();
        uint8_t* pchRequest = (uint8_t*)HeapAlloc(hHeap, 0, BUFSIZE * sizeof(uint8_t));

        DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0;
        BOOL fSuccess = FALSE;

        // Do some extra error checking since the app will keep running even if this thread fails.
        std::list<std::vector<uint8_t>> message_parts;

        if (input_pipe_handle == NULL)
        {
            if (pchRequest != NULL)
                HeapFree(hHeap, 0, pchRequest);
            return;
        }

        if (pchRequest == NULL)
        {
            return;
        }

        // Loop until done reading
        do
        {
            // Read client requests from the pipe. This simplistic code only allows messages
            // up to BUFSIZE characters in length.
            ZeroMemory(pchRequest, BUFSIZE * sizeof(uint8_t));
            fSuccess = ReadFile(
                input_pipe_handle, // handle to pipe
                pchRequest, // buffer to receive data
                BUFSIZE * sizeof(uint8_t), // size of buffer
                &cbBytesRead, // number of bytes read
                NULL); // not overlapped I/O

            if (!fSuccess && GetLastError() != ERROR_MORE_DATA)
            {
                break;
            }
            std::vector<uint8_t> part_vector;
            part_vector.reserve(cbBytesRead);
            std::copy(pchRequest, pchRequest + cbBytesRead, std::back_inserter(part_vector));
            message_parts.push_back(part_vector);
        } while (!fSuccess);

        if (fSuccess)
        {
            // Reconstruct the total_message.
            std::vector<uint8_t> reconstructed_message;
            size_t total_size = 0;
            for (auto& part_vector : message_parts)
            {
                total_size += part_vector.size();
            }
            reconstructed_message.reserve(total_size);
            for (auto& part_vector : message_parts)
            {
                std::move(part_vector.begin(), part_vector.end(), std::back_inserter(reconstructed_message));
            }
            std::wstring unicode_msg;
            unicode_msg.assign(reinterpret_cast<std::wstring::const_pointer>(reconstructed_message.data()), reconstructed_message.size() / sizeof(std::wstring::value_type));
            input_queue.queue_message(unicode_msg);
        }

        // Flush the pipe to allow the client to read the pipe's contents
        // before disconnecting. Then disconnect the pipe, and close the
        // handle to this pipe instance.

        FlushFileBuffers(input_pipe_handle);
        DisconnectNamedPipe(input_pipe_handle);
        CloseHandle(input_pipe_handle);

        HeapFree(hHeap, 0, pchRequest);

        printf("InstanceThread exitting.\n");
    }

    void start_named_pipe_server(HANDLE token)
    {
        // Adapted from https://docs.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server
        const wchar_t* pipe_name = input_pipe_name.c_str();
        BOOL connected = FALSE;
        HANDLE connect_pipe_handle = INVALID_HANDLE_VALUE;
        while (!closed)
        {
            {
                std::unique_lock lock(pipe_connect_handle_mutex);
                connect_pipe_handle = CreateNamedPipe(
                    pipe_name,
                    PIPE_ACCESS_DUPLEX |
                        WRITE_DAC,
                    PIPE_TYPE_MESSAGE |
                        PIPE_READMODE_MESSAGE |
                        PIPE_WAIT,
                    PIPE_UNLIMITED_INSTANCES,
                    BUFSIZE,
                    BUFSIZE,
                    0,
                    NULL);

                if (connect_pipe_handle == INVALID_HANDLE_VALUE)
                {
                    return;
                }

                if (token != NULL)
                {
                    int err = change_pipe_security_allow_restricted_token(connect_pipe_handle, token);
                }
                current_connect_pipe_handle = connect_pipe_handle;
            }
            connected = ConnectNamedPipe(connect_pipe_handle, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
            {
                std::unique_lock lock(pipe_connect_handle_mutex);
                current_connect_pipe_handle = NULL;
            }
            if (connected)
            {
                std::thread(&TwoWayPipeMessageIPC::handle_pipe_connection, this, connect_pipe_handle).detach();
            }
            else
            {
                // Client could not connect.
                CloseHandle(connect_pipe_handle);
            }
        }
    }

    void consume_input_queue_thread()
    {
        while (!closed)
        {
            std::wstring message = input_queue.pop_message();
            if (message.length() == 0)
            {
                break;
            }
            dispatch_inc_message_function(message);
        }
    }
};
